-- Implementation note: WoW's global sin and cos take degrees rather than math.sin/cos radians.
local versionMajor, versionRev = 1, 25;
local max, min, abs, sin, cos = math.max, math.min, math.abs, sin, cos;
local OR_AddonName, OR_Rings, OR_PendingUpdates, OR_KeyBindings
	= "OPie", {}, {}, setmetatable({}, {__index=function(t,k) t[k] = {}; return t[k]; end});
local OneRing = {ext={}};
local configInstance, defaultConfig, unlocked = {}, {DimScreen=false,
	ShowPointer=true, ShowIcons=false, ShowCooldowns=false, ShowCaptions=false, MultiIndication=true, UseGameTooltip=true,
	ClickActivation=false, IndicationOffsetX=0, IndicationOffsetY=0, RingAtMouse=false, FastClickInterval=0.20, TapMouselook=true, ShowOnTap=true,
	MIDisjoint=false, MIScale0=-0.175, MIScale1=0.15, MIScale2=0.05, RingScale=1,
	MIAlpha=false, MIBlend=true, XTPointerSpeed=0, XTScaleSpeed=0, XTZoomTime=0.3}, false;
local PersistentStorageInfo = {};
local invalidatingOptions = {ClickActivation=true, IndicationOffsetX=true, IndicationOffsetY=true, RingAtMouse=true, RingScale=true, FastClickInterval=true};
local waitingToSyncBindings = false;
local defaultProps = {__index={offset=0, mindist=40}};
local LBFGroup;

-- Some basic utility methods
local function assert(condition, text, level, ...)
	return (not condition) and error(tostring(text):format(...), 1 + (level or 1)) or condition;
end
local function SetAttributes(frame, ...)
	assert(frame, "You're doing it wrong.", 2);
	for i=1,select("#", ...), 2 do
		frame:SetAttribute(select(i, ...), select(i+1, ...));
	end
end
local function SetDimensions(frame, w, h)
	frame:SetWidth(w); frame:SetHeight(h or w);
end
local function copy(t, lib, into)
	assert(type(t) == "table" and (lib == nil or type(lib) == "table"), "Syntax: copy(t[, tLib[, into]]);", 2);
	local nt, lib = type(into) == "table" and into or {}, lib or {}; lib[t] = nt;
	if type(into) == "table" then for k, v in pairs(nt) do if t[k] == nil then nt[k] = nil; end end end
	for k, v in pairs(t) do
		if type(v) == "table" then
			nt[k] = lib[v] or copy(v, lib, nt[k]);
		else nt[k] = v; end
	end
	return nt;
end
local companionSpells, parseMacro = {}
local function updateSpellCache()
	for i=1,GetNumCompanions("CRITTER") do
		local  _, _, csID = GetCompanionInfo("CRITTER",i);
		companionSpells[GetSpellInfo(csID)], companionSpells[csID] = csID, i;
	end
	for i=1,GetNumCompanions("MOUNT") do
		local _, _, csID = GetCompanionInfo("MOUNT",i);
		companionSpells[GetSpellInfo(csID)], companionSpells[csID] = csID, -i;
	end
end
do -- function parseMacro(text, binding)
	local compactMacros = setmetatable({}, {__mode="kv"});
	local mod, bindingCache = {}, {};
	local function compact(macro)
		local gen = "";
		for name, args in ("\n"..macro):gmatch("\n([#/]%a+)%s+([^\n]+)") do
			name = name:lower();
			if name == "#show" or name == "#showtooltip" or name == "/cast" or name == "/use" then
				gen = gen .. (gen == "" and "" or "; ") .. args;
			end
		end
		compactMacros[macro] = gen;
		return gen;
	end
	local function fixModifierClause(open, inv, sel, close)
		if mod[sel] then
			return inv == "no" and "" or (open .. "nomod:" .. sel .. close);
		elseif sel == "" then
			local ms = "";
			for k, v in pairs(mod) do
				if not v then ms = ms .. (ms == "" and "" or "/") .. k; end
			end
			return open .. inv .. "mod:" .. ms .. close;
		else
			local ms = "";
			for m in sel:gmatch("[^/]+") do
				if not mod[m] then ms = ms .. (ms == "" and "" or "/") .. m; end
			end
			if ms == "" then
				return inv == "no" and "" or (open .. "nomod:" .. sel .. close);
			end
			return open .. inv .. "mod:" .. ms .. close;
		end
	end
	local function fixConditions(m)
		return m:gsub("([,%s%[])(n?o?)modi?f?i?e?r?:?([^,%s%]]*)([,%s%]])", fixModifierClause);
	end
	local function fixBindingModifiers(conditionedLine, binding)
		if type(binding) ~= "string" or type(conditionedLine) ~= "string" then return conditionedLine; end
		local key = binding .. "#" .. conditionedLine;
		if not bindingCache[key] then
			local bMod = binding:match("^(.-)%-?[^-]+$"):lower();
			mod.ctrl, mod.alt, mod.shift = bMod:match("ctrl") ~= nil, bMod:match("alt") ~= nil, bMod:match("shift") ~= nil;
			bindingCache[key] = bMod and conditionedLine:gsub("%b[]", fixConditions) or conditionedLine;
		end
		return bindingCache[key];
	end
	function parseMacro(text, binding)
	  if type(text) ~= "string" then return; end
		local cl = compactMacros[text] or compact(text);
		if cl ~= "" then
			cl = SecureCmdOptionParse(fixBindingModifiers(cl, binding));
			if not cl then return; end
			return (GetSpellInfo(cl) or companionSpells[cl]) and "spell" or "item", cl;
		end
	end
end
OneRing.xlu = {copy=copy, assert=assert, parseMacro=parseMacro};

-- Spawn UI elements for indication interface
local OR_IndicationFrame = CreateFrame("Frame", "OneRingIndicator", UIParent);
SetDimensions(OR_IndicationFrame, 128); OR_IndicationFrame:SetFrameStrata("FULLSCREEN"); OR_IndicationFrame:SetPoint("CENTER");
local ORI_ConfigCache = {}; OR_IndicationFrame.config = ORI_ConfigCache;

local basepath = "Interface\\AddOns\\" .. OR_AddonName .. "\\";
local OR_IndicationCircle = OR_IndicationFrame:CreateTexture(nil, "ARTWORK");
SetDimensions(OR_IndicationCircle, 64); OR_IndicationCircle:SetPoint("CENTER");
OR_IndicationCircle:SetTexture(basepath .. "gfx\\circle.tga");
OR_IndicationCircle:SetVertexColor(0.1, 0.4, 1, 0.9);

local OR_IndicationPointer = OR_IndicationFrame:CreateTexture(nil, "ARTWORK");
SetDimensions(OR_IndicationPointer, 128); OR_IndicationPointer:SetPoint("CENTER");
OR_IndicationPointer:SetTexture(basepath .. "gfx\\pointer.tga");
OR_IndicationPointer:SetVertexColor(0.1, 0.4, 1, 0.9);

local OR_IndicationGlow = OR_IndicationFrame:CreateTexture(nil, "BACKGROUND");
SetDimensions(OR_IndicationGlow, 128); OR_IndicationGlow:SetPoint("CENTER");
OR_IndicationGlow:SetTexture(basepath .. "gfx\\glow.tga");
OR_IndicationGlow:SetVertexColor(0.1, 0.4, 1, 0.9);

local OR_LogoVersion = OR_IndicationFrame:CreateFontString("OVERLAY", nil, "GameFontHighlight");
OR_LogoVersion:SetPoint("TOP", OR_IndicationCircle, "BOTTOM", 0, -20); OR_LogoVersion:SetJustifyH("CENTER");

local OR_ScreenDim = WorldFrame:CreateTexture(nil, "OVERLAY");
OR_ScreenDim:SetAllPoints(); OR_ScreenDim:SetTexture(0,0,0); OR_ScreenDim:SetAlpha(0.25); OR_ScreenDim:Hide();

local OR_CenterIndication = CreateFrame("Button", "ORSI_CenterContainer", OR_IndicationFrame);
	SetDimensions(OR_CenterIndication, 28); OR_CenterIndication:SetPoint("CENTER");
	local OR_SpellIcon = OR_CenterIndication:CreateTexture(nil, "ARTWORK");
	OR_SpellIcon:SetAllPoints(); OR_SpellIcon:SetAlpha(0.8);
	local OR_SpellCount = OR_CenterIndication:CreateFontString(nil, "OVERLAY", "NumberFontNormalLarge");
	OR_SpellCount:SetPoint("BOTTOMRIGHT", -1, 1);

local OR_SpellCaption = OR_IndicationFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
OR_SpellCaption:SetPoint("TOP", OR_IndicationCircle, "BOTTOM", 0, -15); OR_SpellCaption:SetJustifyH("CENTER");

local OR_SpellCD = OR_IndicationFrame:CreateFontString(nil, "OVERLAY", "NumberFontNormalHuge");
OR_SpellCD:SetJustifyH("CENTER"); OR_SpellCD:SetJustifyV("CENTER"); OR_SpellCD:SetPoint("CENTER", OR_IndicationCircle);

local ORMI_Parent = CreateFrame("Frame", nil, OR_IndicationFrame);
	SetDimensions(ORMI_Parent, 256); ORMI_Parent:SetPoint("CENTER");
local function ORMI_SetAngle(self, angle, radius)
	self:SetPoint("CENTER", radius*cos(90+angle), radius*sin(90+angle));
end
local function ORMI_SetScaleSmoothly(self, scale)
	local old, limit = self:GetScale(), 2^(ORI_ConfigCache.XTScaleSpeed)/GetFramerate();
	self:SetScale(old + min(limit, max(-limit, scale-old)));
end
local function ORMI_SetAlphaSmoothly(self, scale)
	local old, limit = self:GetAlpha(), 2^(ORI_ConfigCache.XTScaleSpeed)/GetFramerate();
	self:SetAlpha(old + min(limit, max(-limit, scale-old)));
end
ORMI_Parent.SetAlphaSmoothly = ORMI_SetAlphaSmoothly;
local OR_MultiIndicators = setmetatable({}, {
	__index=function(t, k)
		local e = CreateFrame("CheckButton", "ORMI_Container" .. k, ORMI_Parent);
		e.icon, e.text, e.count = e:CreateTexture(nil, "ARTWORK"),
			e:CreateFontString(nil, "OVERLAY", "NumberFontNormalHuge"),
			e:CreateFontString(nil, "OVERLAY", "NumberFontNormalLarge");
		e.SetAngle, e.SetScaleSmoothly, e.SetAlphaSmoothly, t[k] = ORMI_SetAngle, ORMI_SetScaleSmoothly, ORMI_SetAlphaSmoothly, e;
		SetDimensions(e, 48); e.icon:SetAllPoints();
		e.text:SetPoint("CENTER");
		e:EnableMouse(false);
		if LBFGroup then
			e:SetDisabledTexture("");
			e:SetHighlightTexture("");
			e:SetCheckedTexture("");
			pcall(LBFGroup.AddButton, LBFGroup, e, {Icon=e.icon, Count=e.count});
		else
			e.count:SetPoint("BOTTOMRIGHT", -4, 4);
		end
		return e;
	end
}); -- end (collapse block)

-- Indication ring updates (pretty shiny things) [for collapse block]
local r2, r42, realUpdate, colorTable = math.sqrt(0.5^2+0.5^2), math.sqrt(42), true, {[4]=0.9};
local function ORI_SetViewAngle(ofs, angle)
	OR_IndicationCircle:SetTexCoord(
		0.5+r2*cos(ofs+135), 0.5+r2*sin(ofs+135),
		0.5+r2*cos(ofs-135), 0.5+r2*sin(ofs-135),
		0.5+r2*cos(ofs+45), 0.5+r2*sin(ofs+45),
		0.5+r2*cos(ofs-45), 0.5+r2*sin(ofs-45)
	);
	OR_IndicationPointer:SetTexCoord(
		0.5+r2*cos(angle+135), 0.5+r2*sin(angle+135),
		0.5+r2*cos(angle-135), 0.5+r2*sin(angle-135),
		0.5+r2*cos(angle+45), 0.5+r2*sin(angle+45),
		0.5+r2*cos(angle-45), 0.5+r2*sin(angle-45)
	);
	OR_IndicationGlow:SetTexCoord(
		0.5+r2*cos(ofs+135), 0.5+r2*sin(ofs+135),
		0.5+r2*cos(ofs-135), 0.5+r2*sin(ofs-135),
		0.5+r2*cos(ofs+45), 0.5+r2*sin(ofs+45),
		0.5+r2*cos(ofs-45), 0.5+r2*sin(ofs-45)
	);
end
local function ORI_GetActionInfoEntry(e, binding)
	if type(e) ~= "table" then return true, 0, nil, nil; end
	if type(e.actionHint) == "function" then return e.actionHint(e[1], e[2], e[3]); end

	local caption, icon, usable, cooldown, cd, t, n, enabled = e.caption, e.icon;
	if type(caption) == "function" then caption = e:caption(); end
	if type(icon) == "function" then icon = e:icon(); end

	-- Attempt to determine what this button does.
	local btype, bcontent, oicon, ocaption, _ = e[1], e[2], icon, caption;
	if btype == "macrotext" then btype, bcontent = parseMacro(bcontent, binding); end
	if btype == "spell" or btype == "item" then t, n = btype, bcontent;
	elseif btype == "macro" then
		t, n = "spell", GetMacroSpell(bcontent);
		if not n then	t, n = "item", GetMacroItem(bcontent); end
		if not n then
			local name, icon = GetMacroInfo(bcontent);
			return true, 0, icon, name;
		end
	end
	if not n then return true, 0, oicon or icon or nil, ocaption or caption, 0; end -- Failed, decry own inadequacy.

	-- Filter that pesky item identifier
	if t == "item" and btype ~= "macro" then n = SecureCmdItemParse(n); caption = n and GetItemInfo(n); end
	if t == "item" and btype == "macro" then _, n = GetItemInfo(n); end

	-- Now determine whether the action is ready
	if t == "spell" then
		usable, icon, cooldown, cd, enabled = IsUsableSpell(n), GetSpellTexture(n), GetSpellCooldown(n);
	elseif t == "item" then
		usable, icon, cooldown, cd, enabled = IsUsableItem(n), GetItemIcon(n), GetItemCooldown(n);
	end
	if t == "spell" and companionSpells[n] then
		local cId, cType = companionSpells[companionSpells[n]];
		cType, cId = cId > 0 and "CRITTER" or "MOUNT", abs(cId);
		usable, icon, cooldown, cd, enabled = cType == "CRITTER" or (not (InCombatLockdown() or IsIndoors())), select(4, GetCompanionInfo(cType, cId)), GetCompanionCooldown(cType, cId);
	end
	if cooldown and cooldown ~= 0 then cooldown = cooldown + cd - GetTime(); end
	enabled = enabled == 0;
	usable = not not (usable and (cooldown == nil or cooldown == 0) or enabled);

	-- And the number of times this can be used
	local count = 0;
	if t == "item" and IsConsumableItem(n) then count = GetItemCount(n);
	elseif t == "spell" then count = GetSpellCount(n);
	end

	return usable, enabled and 0 or cooldown, oicon or icon, ocaption or caption or n or "", count or 0, t, n;
end
local function ORI_Cooldown(cd)
	if cd == 0 or not cd then return ""; end
	local f, n, unit = cd > 10 and "%d%s" or "%.1f", cd, "";
	if n > 86400 then n, unit = ceil(n/86400), "d";
	elseif n > 3600 then n, unit = ceil(n/3600), "h";
	elseif n > 60 then n, unit = ceil(n/60), "m";
	elseif n > 10 then n = ceil(n); end
	return f, n, unit;
end
local function SecurePieButton_GetIndex(x, y, slices, mindist, offset)
	local radius, segAngle = (x*x + y*y)^0.5, 360 / slices;
	if radius < mindist or slices <= 0 then return false; end
	local angle = (math.deg(math.atan2(x, y)) + segAngle/2 - offset) % 360;
	return floor(angle / segAngle) + 1;
end
local function ORI_Update(self, elapsed)
	local time, config, ring, props = GetTime(), self.config, self.ring, OR_Rings[self.ringID];
	if self.effect == "zoomout" then
		local delta = max(0, (self.eend-time)/config.XTZoomTime);
		if delta == 0 then return self:Hide(), OR_ScreenDim:Hide(); end
		self:SetScale(config.RingScale*delta); self:SetAlpha(delta);
		OR_ScreenDim:SetAlpha(delta*0.20);
		return;
	elseif self.effect == "zoomin" then
		local delta = max(0, (self.eend-time)/config.XTZoomTime);
		if delta == 0 then self.effect = nil; end
		if delta < 1 then	self:SetScale(config.RingScale/max(0.2,cos(65*delta))); self:SetAlpha(1-delta); end
		OR_ScreenDim:SetAlpha(0.20-delta*0.20);
	end
	if not config.ShowPointer then SetCursor("HIDE"); end

	local scale, l, b, w, h = ring:GetEffectiveScale(), ring:GetRect();
	if not (l and b and w and h) then
		OR_IndicationCircle:SetVertexColor(1,0,0,0.5);
		OR_IndicationGlow:SetVertexColor(1,0,0,0.8);
		OR_IndicationPointer:SetVertexColor(1,0,0,0.2);
		self.effect, self.eend = "zoomout", GetTime() + 1.5;
		return;
	end
	local x, y = GetCursorPosition();
	local dx, dy = (x / scale) - (l + w / 2), (y / scale) - (b + h / 2);

	-- Calculate pointer location
	local angle, isInFastClick = (math.deg(math.atan2(dx, dy)) -180) % 360, self.fastClickExpire > time and self.fastClickSlice;
	if isInFastClick then
		angle = props.offset + (self.fastClickSlice-1)*360/#props - 180;
	end

	local oangle = (not isInFastClick) and self.angle or angle;
	local adiff = math.min((angle-oangle) % 360, (oangle-angle) % 360);
	local arate = 180;
	if adiff > 60 then
		arate = 420 + 120*sin(min(90, adiff-60));
	elseif adiff > 15 then
		arate = 180 + 240*sin(min(90, max((adiff-15)*2, 0)));
	else
		arate = 20 + 160*sin(min(90, adiff*6));
	end
	local abound, arotDirection = arate/GetFramerate(), ((oangle - angle) % 360 < (angle - oangle) % 360) and -1 or 1;
	abound = abound * 2^config.XTPointerSpeed;
	if adiff < abound then
		self.angle = angle;
	else
		self.angle = (oangle + arotDirection * abound) % 360;
	end
	ORI_SetViewAngle(time*180, self.angle);

	-- What is selected?
	local si, osi = isInFastClick and self.fastClickSlice or SecurePieButton_GetIndex(dx, dy, #props, props.mindist, props.offset), self.oldValue;
	local usable, cd, icon, caption, count, nature, ident = nil, false;
	if si then
		usable, cd, icon, caption, count, nature, ident = ORI_GetActionInfoEntry(props[si], props.boundKey);
	end
	if si ~= osi then
		if props[si] then
			colorTable[1], colorTable[2], colorTable[3] = props[si].r or 0, props[si].g or 0, props[si].b or 0;
			OR_IndicationPointer:SetVertexColor(unpack(colorTable));
			OR_IndicationCircle:SetVertexColor(unpack(colorTable));
			OR_IndicationGlow:SetVertexColor(unpack(colorTable, 1, 3));
			OR_SpellCaption:SetTextColor(unpack(colorTable,1,3));
			OR_SpellCD:SetTextColor(unpack(colorTable));
			if config.UseGameTooltip then
				local ifunc, iarg;
				GameTooltip_SetDefaultAnchor(GameTooltip, UIParent);
				if nature == "item" then ifunc, iarg = GameTooltip.SetHyperlink, select(2,GetItemInfo(ident)) or ident;
				elseif nature == "spell" and ident then ifunc, iarg = GameTooltip.SetHyperlink, companionSpells[ident] and ("spell:" .. companionSpells[ident]) or GetSpellLink(ident);
				else ifunc, iarg = GameTooltip.Hide, false; end
				if not (ifunc and iarg) and caption then ifunc, iarg = GameTooltip.AddLine, caption; end
				if ifunc and iarg then
					if pcall(ifunc, GameTooltip, iarg) then
						GameTooltip:Show();
					end
				end
			end
		else
			OR_IndicationPointer:SetVertexColor(1,1,1,0.1);
			OR_IndicationCircle:SetVertexColor(1,1,1,0.3);
			OR_IndicationGlow:SetVertexColor(0.75,0.75,0.75);
			GameTooltip:Hide();
		end
		self.oldValue = si;
	end
	if icon then
		OR_SpellIcon:SetTexture(icon);
		if not LBGroup then
			OR_SpellIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92);
		end
	end
	OR_SpellCD:SetFormattedText(ORI_Cooldown(config.ShowCooldowns and cd or 0));
	OR_CenterIndication[icon and config.ShowIcons and (not config.MultiIndication) and "Show" or "Hide"](OR_CenterIndication);
	OR_SpellCaption:SetText(config.ShowCaptions and caption or "");
	OR_SpellCount:SetText(icon and config.ShowIcons and (not config.MultiIndication) and count and count > 0 and count or "");

	-- Glow animation block
	usable = usable == true; -- nil makes binary logic cry
	local gAnim, gEnd, oIG = self.gAnim, self.gEnd, self.oldIsGlowing;
	if usable ~= oIG then
		gAnim = usable and "in" or "out";
		if gEnd and gEnd > time then
			gEnd = time+0.5-(gEnd-time);
		else
			gEnd = time+0.5;
		end
		self.oldIsGlowing, self.gAnim, self.gEnd = usable, gAnim, gEnd;
		OR_IndicationGlow:Show();
	end
	if not gAnim then
	elseif gEnd <= time then
		self.gAnim, self.gEnd = nil, nil;
		OR_IndicationGlow[gAnim == "in" and "Show" or "Hide"](OR_IndicationGlow);
		OR_IndicationGlow:SetAlpha(sin(150));
	else
		local pg, base, shift = 1-(gEnd-time)/0.5,
			gAnim == "out" and 150 or 180,
			gAnim == "out" and 30 or -150;
		OR_IndicationGlow:SetAlpha(sin(base+shift*pg));
	end

	-- Multiple indication
	if config.MultiIndication then
		local cmState, mut = (IsShiftKeyDown() and 1 or 0) + (IsControlKeyDown() and 2 or 0) + (IsAltKeyDown() and 4 or 0), (self.schedMultiUpdate or 0);
		local astep = 360/#props;
		if self.omState ~= cmState or mut >= 0  then
			self.omState, self.schedMultiUpdate = cmState, -0.05;
			for i=1,#props do
				local usable, cd, icon, caption, count = ORI_GetActionInfoEntry(props[i], props.boundKey);
				OR_MultiIndicators[i].icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark");
				OR_MultiIndicators[i].text:SetFormattedText(ORI_Cooldown(config.ShowCooldowns and cd or 0));
				OR_MultiIndicators[i].count:SetText(count and count > 0 and count or "");
				SetDesaturation(OR_MultiIndicators[i].icon, (cd or 0) > 0 and 1 or nil);
				OR_MultiIndicators[i][(cd or 0) > 0 and "Disable" or "Enable"](OR_MultiIndicators[i]);
			end
		else
			self.schedMultiUpdate = mut + elapsed;
		end

		local cangle = config.MIDisjoint and ((angle - 180) % 360) or ((self.angle - 180) % 360);
		cangle = (cangle - props.offset) % 360;
		for i=1,#props do
			local adiff = math.min((astep*(i-1)-cangle) % 360, (cangle-astep*(i-1)) % 360);
			if config.MIAlpha then
				local sdiff = adiff / 360 * #props;
				local a = 0.45*cos(math.min(sdiff*30,90))+0.25*cos(math.min(sdiff*90, 90))+0.15;
				if i == si then a = 1; end
				OR_MultiIndicators[i]:SetAlphaSmoothly(si and a or 0.5);
			elseif not si then
				OR_MultiIndicators[i]:SetScaleSmoothly(max(0.1, 1+config.MIScale0));
			else
				local mmul = #props > 5 and #props/6 or 1.25
				if adiff <= astep / 2 then
					local toprange = math.max(10, astep / 6);
					local range, delta = astep / 2 - toprange, adiff - toprange;
					OR_MultiIndicators[i]:SetScaleSmoothly(max(0.1, 1 + config.MIScale1 + config.MIScale2*cos(max(0, delta) * 90 / range)));
				elseif adiff <= astep * mmul then
					local range, delta = astep * mmul - astep / 2, adiff - astep / 2;
					OR_MultiIndicators[i]:SetScaleSmoothly(max(0.1, 1 + config.MIScale1*cos(delta * 90 / range)));
				else
					OR_MultiIndicators[i]:SetScaleSmoothly(1);
				end
			end
		end
		if config.MIAlpha then
			ORMI_Parent:SetAlphaSmoothly(props[si] and 1 or 0.35);
		end
		if si ~= osi then
			if osi then OR_MultiIndicators[osi]:UnlockHighlight(); end
			if si then OR_MultiIndicators[si]:LockHighlight(); end
		end
	end
end
local function ORI_FinishSplash(self)
	self:SetScript("OnUpdate", ORI_Update); self:Hide();
	OR_LogoVersion:Hide(); realUpdate = false;
	local LBF = LibStub and LibStub("LibButtonFacade",true);
	if LBF then
		local cfg = configInstance.LBFConfig;
		LBFGroup = LBF:Group(OR_AddonName);
		LBFGroup:AddButton(OR_CenterIndication, {Icon=OR_SpellIcon, Count=OR_SpellCount});
		if type(cfg) == "table" then
			LBFGroup.Colors = cfg.Colors or LBFGroup.Colors;
			LBFGroup:Skin(cfg.SkinID, cfg.Gloss, cfg.Backdrop);
		end
	end
end
local function ORI_Splash(self, elapsed)
	local frames, expire = self.frames, self.expire;
	if expire then
		local tl = max(0, expire - GetTime());
		if tl == 0 then
			ORI_FinishSplash(self);
		else
			local weighed = (tl > 0.55) and (tl/0.55*0.5) or (tl < 0.45 and (tl/0.45*0.5) or 0.5);
			ORI_SetViewAngle(GetTime()*180, 180 - 180*tl);
			self:SetAlpha(0.7*sin(180*weighed));
		end
	elseif frames == 84 then
		self.frames, self.expire = nil, GetTime()+1;
	elseif not frames then
		self.frames = 1;
		self:SetAlpha(0);
		OR_LogoVersion:SetText(GetAddOnMetadata(OR_AddonName, "X-Version-Name-Wrath") or "");
	else
		self.frames = frames+ 1;
	end
end
local ORI_RingRadius = setmetatable({}, {
	__index=function(t,n)
		local radius, sideLength, abs, astep = 95, 48, math.abs, 360 / n;
		repeat
			local ox, oy, clear = radius, 0, true;
			for i=1,n-1 do
				local nx, ny = radius*cos(i*astep), radius*sin(i*astep);
				if abs(ox - nx) < sideLength and abs(oy - ny) < sideLength then
					clear, radius = false, radius + 5;
					break;
				end
				ox, oy = nx, ny;
			end
		until clear;
		t[n] = radius;
		return radius;
	end
});
local ORI_ActiveRing;
local function ORI_ShowRing(ring, ignoreFC)
	local frame, props = OR_IndicationFrame, OR_Rings[ring.ringName];
	ORI_ActiveRing = ring;
	if realUpdate then
		-- Early trigger: splash animation is still active, get rid of it.
		ORI_FinishSplash(frame);
	end

	-- Copy ring configuration to indication frame.
	frame.ring, frame.ringID = ring, ring.ringName;
	for k, v in pairs(defaultConfig) do
		frame.config[k] = configInstance.RingOptions[frame.ringID .. "#" .. k];
	end

	-- Zoom in to the ring's indication option
	frame.effect, frame.eend = "zoomin", GetTime() + frame.config.XTZoomTime;

	-- Move pointer to match cursor position, reset glow
	local scale, l, b, w, h = ring:GetEffectiveScale(), ring:GetRect();
	local maxdist, x, y = max(w or 5000, h or 5000) / 2, GetCursorPosition();
	local dx, dy = (x / scale) - ((l or 0) + (w or 0) / 2), (y / scale) - ((b or 0) + (h or 0) / 2);
	frame.angle, frame.oldValue = (math.deg(math.atan2(dx, dy)) -180) % 360, -1; -- force glow update
	frame.oldShown, frame.oldIsGlowing, frame.gAnim = nil, true, nil;
	OR_IndicationGlow:Show(); OR_IndicationGlow:SetAlpha(sin(150));

	-- Show/Hide multiple indication icons as required
	frame.schedMultiUpdate = 1;
	frame.fastClickExpire, frame.fastClickSlice = ((not ignoreFC) and tonumber(ring:GetAttribute("ORL-FastClickInterval")) or 0)+GetTime(), tonumber(ring:GetAttribute("ORL-FastClickSlice")) or nil;
	local useMultipleIndication, astep = frame.config.MultiIndication, -360/#props;
	local radius = ORI_RingRadius[#props];
	for i=1,(useMultipleIndication and #props or 0) do
		local indic = OR_MultiIndicators[i];
		indic.text:SetText("");
		indic.text:SetTextColor(props[i].r or 1, props[i].g or 1, props[i].b or 1);
		indic:ClearAllPoints();
		indic:SetAngle((i - 1) * astep - props.offset, radius);
		indic:SetScale(1);
		indic:Show();
		if not LBFGroup then
			indic.icon:SetBlendMode(frame.config.MIBlend and "ADD" or "BLEND");
		end
	end
	for i=(useMultipleIndication and (#props+1) or 1),#OR_MultiIndicators do
		OR_MultiIndicators[i]:Hide();
	end
	for i=1,#OR_MultiIndicators do OR_MultiIndicators[i]:SetAlpha(1); end
	ORMI_Parent:SetAlpha(1);

	-- Show the indication frame
	frame:ClearAllPoints(); frame:SetPoint("CENTER", ring);
	frame:SetScale(math.max(0.01,frame.config.RingScale)); frame:Show();
	if frame.config.DimScreen then OR_ScreenDim:Show(); end
	if not frame.config.TapMouselook then pcall(MouselookStop); end
end
local function ORI_HideRing(ring)
	if ORI_ActiveRing == ring then
		OR_IndicationFrame.effect, OR_IndicationFrame.eend = "zoomout", GetTime() + ORI_ConfigCache.XTZoomTime;
		SetCursor(nil);
		if OR_IndicationFrame.fastClickExpire > GetTime() then
			OR_IndicationFrame:Hide();
		end
		GameTooltip:Hide();
	end
end
OR_IndicationFrame:SetScript("OnUpdate", ORI_Splash);

-- Ring creation and management core
local OR_RingButtons, OR_RingHandlerParent = {}, CreateFrame("Frame", nil, UIParent, "SecureHandlerBaseTemplate");
local function OR_GetRingOption(ringName, option)
	local v = configInstance.RingOptions[ringName .. "#" .. option];
	if v == nil then
		v = configInstance[option];
	end
	return v;
end
function OR_RingHandlerParent:CustomClick(ring, slice)
	local propsEntry = OR_Rings[OR_Rings[ring]][slice];
	if propsEntry and propsEntry[1] == "func" and type(propsEntry[2]) == "function" then
		pcall(propsEntry[2], ring, propsEntry[3]);
	end
end
function OR_RingHandlerParent:RingState(state, ring, slice)
	local element, button = OR_Rings[OR_Rings[ring]], OR_RingButtons[OR_Rings[ring]];
	if state == "click" then
		if slice ~= nil then element = element[slice]; end
		if type(element.preClick) ~= "function" then return; end
	elseif state == "activate" then
		if type(element.preClick) == "function" then
			local ok, err = pcall(element.preClick, ring, slice);
			if not ok then
				local domain = OR_Rings[ring] .. (slice and ("[%d]"):format(slice) or "") .. ".preClick";
				EC_Error("OPie", domain, err:gsub("Interface\\AddOns\\"..OR_AddonName.."\\", ""));
			end
		end
		if OR_GetRingOption(OR_Rings[ring], "ShowOnTap") then
			ORI_ShowRing(button);
			if not OR_GetRingOption(OR_Rings[ring], "TapMouselook") then
				MouselookStop();
			end
		end
	elseif state == "fcDone" and not OR_GetRingOption(OR_Rings[ring], "ShowOnTap") then
		ORI_ShowRing(button, true);
		MouselookStop();
	end
end
local function OR_CreateRingButton(t, name)
	local button = CreateFrame("Button", "ORTB__" .. name:gsub(":","COLON"), UIParent, "SecureActionButtonTemplate");
	SetDimensions(button, 9001);
	button:SetFrameStrata("FULLSCREEN");
	button:RegisterForClicks("AnyUp", "AnyDown");
	button:Hide();
	button:SetID(OR_Rings[name].ringID);
	button:SetScript("OnHide", ORI_HideRing);
	OR_RingHandlerParent:WrapScript(button, "OnClick", "return control:RunFor(self, ORL_ProcessClick, button, down)", "if message == 'Hide' then control:RunFor(self, ORL_DoHide) end");
	t[name], button.ringName = button, name;
	return button;
end
do -- Initialize the common ring header
	local function OR_AddSecureHandlerFunction(name, body)
		OR_RingHandlerParent:Execute(("%s = %q;"):format(name, body));
	end
	OR_AddSecureHandlerFunction("ORL_DoHide", [[
		self:Hide();
		self:ClearBinding('BUTTON2');
		self:ClearBinding('ESCAPE');
		self:SetAttribute('type', nil);
		if activeButton == self then activeButton = nil; end
	]]);
	OR_AddSecureHandlerFunction("ORL_DisableFastClick", [[
		if fastClickable and message == generation and activeButton then
			control:CallMethod("RingState", "fcDone", activeButton:GetID());
		end
		fastClickable = fastClickable and not (message == generation);
	]]);
	OR_AddSecureHandlerFunction("ORL_CheckVisibility", [[local button, down, clickActivated = ...;
		local selfIsShown = self == activeButton;
		if button == "use" and down and not selfIsShown then
			if activeButton then control:RunFor(activeButton, ORL_DoHide); end
			self:SetPoint('CENTER', self:GetAttribute("ORL-OffsetRel"), self:GetAttribute("ORL-OffsetX"), self:GetAttribute("ORL-OffsetY"));
			control:CallMethod("RingState", "activate", self:GetID());
			generation = (generation or 0) + 1;
			self:Show(); selfIsShown = true;
			self:SetBindingClick(true, "BUTTON2", self:GetName(), "close");
			self:SetBindingClick(true, "ESCAPE", self:GetName(), "close");
			local fastClickInterval, fastClickSlice = tonumber(self:GetAttribute("ORL-FastClickInterval")) or 0,tonumber(self:GetAttribute("ORL-FastClickSlice")) or 0;
			fastClickable, activeButton = false and fastClickInterval > 0 and fastClickSlice ~= 0, self;
			if fastClickable then
				control:SetTimer(fastClickInterval, generation, ORL_DisableFastClick);
			else
				control:CallMethod("RingState", "fcDone", self:GetID());
			end
			return true, false;
		elseif (clickActivated and button == "use" and down) or button == "RightButton" or button == "close" or (button ~= "use" and not clickActivated) then
			control:RunFor(self, ORL_DoHide); selfIsShown = false;
			return true, false;
		elseif (clickActivated and button ~= "LeftButton") then
			return false, false;
		end
		return false, selfIsShown;
	]]);
	OR_AddSecureHandlerFunction("ORL_GetSelectedSlice", [[local mindist, slices, offset = ...;
		if fastClickable then return self:GetAttribute("ORL-FastClickSlice"); end
		local x, y = self:GetMousePosition();
		if not (x and y) then return false; end
		x, y = x - 0.5, y - 0.5;

		local radius, segAngle = (x*x*9001^2 + y*y*9001^2)^0.5, 360/slices;
		if radius < mindist or slices <= 0 then return false; end
		local angle = (math.deg(math.atan2(x, y)) + segAngle/2 - offset) % 360;
		return floor(angle / segAngle) + 1;
	]]);
	OR_AddSecureHandlerFunction("ORL_ProcessClick", [[local button, down = ...;
		local isHandled, isActivating = control:RunFor(self, ORL_CheckVisibility, button, down, self:GetAttribute("ORL-ClickActivation"));
		if isHandled or not isActivating then return false; end

		local selectedSlice = control:RunFor(self, ORL_GetSelectedSlice, self:GetAttribute("piemindist") or 0,
			self:GetAttribute("piecount"), self:GetAttribute("pieoffset") or 0);
		if not selectedSlice then return nil, "Hide"; end

		control:CallMethod("RingState", "click", self:GetID(), selectedSlice);
		if self:GetAttribute("*type-s"..selectedSlice) == "func" then
			control:CallMethod("CustomClick", self:GetID(), selectedSlice);
			return nil, "Hide";
		end
		if self:GetAttribute("ORL-FastClickUpdate") then
			self:SetAttribute("ORL-FastClickSlice", selectedSlice);
		end
		return "s" .. selectedSlice, "Hide";
		]]);
	setmetatable(OR_RingButtons, {__index=OR_CreateRingButton});
end
local function OR_SyncRing(name, props)
	if InCombatLockdown() or not unlocked then
		if not OR_Rings[name] then
			OR_Rings[name], OR_Rings[#OR_Rings+1] = {ringID=#OR_Rings+1}, name;
		end
		OR_PendingUpdates[name] = props;
		return OR_Rings[name].ringID, false, "locked";
	end

	-- Keep a local copy of the properties table, lest someone changes it while we're not looking
	local op, ringID, isSimpleUpdate = OR_Rings[name], OR_Rings[name] and OR_Rings[name].ringID or (#OR_Rings+1), op == props;
	OR_Rings[name] = isSimpleUpdate and OR_Rings[name] or copy(props, nil, OR_Rings[name]);
	OR_Rings[name].ringID, OR_Rings[ringID] = ringID, name;
	props = setmetatable(OR_Rings[name], defaultProps); -- use our copy from here on forward
	if not isSimpleUpdate then
		for i, e in ipairs(props) do
			if e[1] == "func" and type(e[3]) == "table" then
				e[3] = copy(e[3]); -- Pure paranoia: this prevents a potential reference leak.
			end
		end
	end

	-- Configure the pie button. OR_RingButtons[] creates and caches them based on name.
	local ringBtn = OR_RingButtons[name];
	ringBtn:SetAttribute("ORL-OffsetRel", OR_GetRingOption(name, "RingAtMouse") and "$cursor" or "$screen");
	SetAttributes(ringBtn, "ORL-OffsetX",OR_GetRingOption(name, "IndicationOffsetX"), "ORL-OffsetY",-OR_GetRingOption(name, "IndicationOffsetY"), "ORL-ClickActivation", OR_GetRingOption(name, "ClickActivation"));
	ringBtn:SetScale(OR_GetRingOption(name, "RingScale"));

	for i=1,#props do
		local sliceType, actionID, target, mask = props[i][1], props[i][2], props[i][3], "*%s-s" .. i;
		if (sliceType == "macro" and (type(actionID) == "number" or type(actionID) == "string")) or
			 (sliceType == "macrotext" and type(actionID) == "string") then
			SetAttributes(ringBtn, mask:format("type"),"macro", mask:format(sliceType),actionID);
		elseif (sliceType == "item" or sliceType == "spell") and (type(actionID) == "number" or type(actionID) == "string") then
			SetAttributes(ringBtn, mask:format("type"),sliceType, mask:format(sliceType),actionID, mask:format("target"),target or "player");
		elseif sliceType == "func" and type(actionID) == "function" then
			ringBtn:SetAttribute(mask:format("type"), "func");
		else
			assert(false, "Slice %d for ring %q is invalid (%q, %q)", 2, i, name, tostring(sliceType), tostring(actionID));
		end
	end
	SetAttributes(ringBtn, "piecount",#props, "piemindist",props.mindist, "pieoffset",props.offset);

	-- FastClick behavior
	local enableFC = type(props.fastClickSlice) == "number" and props.fastClickSlice >= 0 and props.fastClickSlice <= #props and props.fastClickSlice or false;
	SetAttributes(ringBtn, "ORL-FastClickInterval", enableFC and OR_GetRingOption(name, "FastClickInterval") or 0, "ORL-FastClickSlice", enableFC and props.fastClickSlice or 0, "ORL-FastClickUpdate", enableFC and props.fastClickUpdate and true or nil);

	-- Clear previous hotkey ownership, clean up hotkey tables
	for k, v in pairs(OR_KeyBindings) do
		for k2, v2 in pairs(v) do
			if v2 == name then v[k2] = nil; end
		end
		if not next(v) then
			OR_KeyBindings[k] = nil;
		end
	end

	-- Set hotkey, if applicable.
	local hotkey, override = configInstance.Bindings[name], true; -- false: disabled
	if hotkey == nil then hotkey, override = props.hotkey, false; end
	if hotkey and GetBindingAction(hotkey) ~= "" and not override then hotkey = nil; end
	if hotkey then
		local owner = OR_KeyBindings[hotkey];
		for i=1,2 do
			local wanted = bit.band(i, props.hotkeyState or 3) ~= 0;
			if wanted and (owner[i] == nil or (override and not configInstance.Bindings[owner[i]])) then
				owner[i] = name;
			end
		end
	end
	if hotkey and OR_KeyBindings[hotkey][1] ~= name then hotkey = nil; end
	ClearOverrideBindings(ringBtn);
	if type(hotkey) == "string" and bit.band(1, props.hotkeyState or 3) ~= 0 then
		SetOverrideBindingClick(ringBtn, false, hotkey, OR_RingButtons[name]:GetName(), "use");
	end
	props.boundKey = hotkey;

	return ringID, ringBtn;
end

-- Responding to WoW Events
local function OR_NotifyPVars(event, filter)
	for k, v in pairs(PersistentStorageInfo) do
		local n = PersistentStorageInfo[v];
		if type(k) == "string" and type(v) == "table" and type(n) == "function" and (v == (filter or v)) then
			local ok, err = pcall(n, event, k, v);
			if not ok then
				EC_Error(OR_AddonName, "NotifyPVar:" ..k, err);
			end
		end
	end
end
local function OR_SyncProps()
	if not InCombatLockdown() then
		for k, props in pairs(OR_PendingUpdates) do
			OR_SyncRing(k, props);
			OR_PendingUpdates[k] = nil;
		end
	end
end
local function OR_CheckBindings()
	if InCombatLockdown() then waitingToSyncBindings = true; return; end
	waitingToSyncBindings = false;
	local updated = false;
	for k, v in pairs(OR_Rings) do
		local key = v.hotkey;
		if configInstance.Bindings[k] == nil and type(key) == "string" then
			local isConflict, isActive = GetBindingAction(key) ~= "", OR_KeyBindings[key] and (OR_KeyBindings[key][1] == k or OR_KeyBindings[key][2] == k);
			if isConflict == isActive then OR_PendingUpdates[k], updated = v, true; end
		end
	end
	if updated then OR_SyncProps(); end
end
local function OR_SwitchCombatState(event)
	if event == "PLAYER_REGEN_ENABLED" then
		OR_SyncProps();
	end
	local flag = (event == "PLAYER_REGEN_ENABLED") and 1 or 2;
	for k, v in pairs(OR_Rings) do
		if type(v) == "table" and bit.band(v.hotkeyState or 3, flag) == 0 then
			-- This one is needs a clear, so we just do that regardless.
			ClearOverrideBindings(OR_RingButtons[k]);
		elseif v.hotkeyState and v.hotkeyState ~= 3 then
			-- This one wants a bind, so let's see if it owns the key first.
			local key = OneRing:GetRingBinding(v.ringID);
			if key and OR_KeyBindings[key][flag] == k then
				SetOverrideBindingClick(OR_RingButtons[k], false, key, OR_RingButtons[k]:GetName(), "use");
			end
		end
		if waitingToSyncBindings then
			OR_CheckBindings();
		end
	end
end
local function OR_InitConfigState()
	-- Get saved vars, set defaults as defaults.
	setmetatable(configInstance, {__index=defaultConfig});
	setmetatable(ORI_ConfigCache, {__index=configInstance});
	if type(OneRing_Config) == "table" then
		for k, v in pairs(OneRing_Config) do
			configInstance[k] = v;
		end
	end

	-- Backwards compatibility.
	configInstance.PersistentStorage, configInstance.SavedRingData = configInstance.PersistentStorage or configInstance.SavedRingData, nil;

	-- And a few special tables.
	if type(configInstance.PersistentStorage) ~= "table" then configInstance.PersistentStorage = {}; end
	if type(configInstance.Bindings) ~= "table" then configInstance.Bindings = {}; end
	if type(configInstance.RingOptions) ~= "table" then configInstance.RingOptions = {}; end

	-- Load variables into relevant tables, unlock core and fire notifications.
	for k, v in pairs(configInstance.PersistentStorage) do
		if PersistentStorageInfo[k] then
			copy(v, nil, PersistentStorageInfo[k]);
		end
	end
	OneRing_Config, unlocked = nil, true;
	OR_NotifyPVars("LOADED");
	OR_SyncProps();
end
local function OR_LibState(event, addon)
	if event == "ADDON_LOADED" and addon == OR_AddonName then
		OR_InitConfigState();
	elseif not unlocked then
		return;
	end
	if event == "PLAYER_LOGIN" then
		updateSpellCache();
		OR_NotifyPVars("LOGIN");
		OR_SyncProps();
	elseif event == "PLAYER_LOGOUT" then
		OneRing_Config = configInstance;
		OR_NotifyPVars("LOGOUT");
		configInstance.LBFConfig = LBFGroup and {SkinID=LBFGroup.SkinID, Gloss=LBFGroup.Gloss, Backdrop=LBFGroup.Backdrop, Colors=LBFGroup.Colors} or configInstance.LBFConfig or nil;
		for k, v in pairs(configInstance) do
			if v == defaultConfig[k] then
				configInstance[k] = nil;
			end
		end
		for k, v in pairs(PersistentStorageInfo) do
			if type(k) == "string" and type(v) == "table" then
				configInstance.PersistentStorage[k] = next(v) and v or nil;
			end
		end
		if configInstance.RingOptions and not next(configInstance.RingOptions) then configInstance.RingOptions = nil; end
	end
	return "remove";
end

-- Public API
function OneRing:CreateRing(name, props)
	assert(type(name) == "string" and type(props) == "table", "Syntax: header, button = OneRing:CreateRing(\"name\", propsTable)", 2);
	assert(OR_Rings[name] == nil and OR_PendingUpdates[name] == nil, "Ring name %q is already created.", 2, name);
	return OR_SyncRing(name, props);
end
function OneRing:SetRingData(id, props)
	assert(type(id) == "number" and type(props) == "table", "Syntax: OneRing:SetRingData(index, propsTable);", 2);
	local key = assert(OR_Rings[id], "Ring index out of range.", 2);
	return OR_SyncRing(key, props);
end
function OneRing:GetNumRings()
	return #OR_Rings;
end
function OneRing:GetRingData(id)
	assert(type(id) == "number", "Syntax: propsTable = OneRing:GetRingData(index);", 2);
	local key = assert(OR_Rings[id], "Ring index out of bounds", 2);
	local ret = copy(OR_PendingUpdates[key] or OR_Rings[key]);
	ret.codename = key;
	return ret;
end
function OneRing:GetRingInfo(id)
	assert(type(id) == "number", "Syntax: name, key, numEntries = OneRing:GetRingData(index);", 2);
	local key = assert(OR_Rings[id], "Ring index out of bounds", 2);
	local ret = OR_PendingUpdates[key] or OR_Rings[key];
	return ret.name, key, #ret;
end
function OneRing:GetOption(option, ringID)
	assert(type(option) == "string" and (ringID == nil or type(ringID) == "number"), "Syntax: value = OneRing:GetOption(\"option\"[, ringID]);", 2);
	assert(defaultConfig[option] ~= nil, "Option %q is unknown.", 2, option);
	return OR_GetRingOption(assert(ringID == nil and "" or OR_Rings[ringID], "Ring index out of bounds", 2), option);
end
function OneRing:SetOption(option, ...)
	local id, value = ...;
	if select("#", ...) == 1 then id, value = nil, ...; end
	assert(type(option) == "string" and (id == nil or type(id) == "number"), "Syntax: OneRing:SetOption(\"option\"[, id], value or nil);", 2);
	assert(defaultConfig[option] ~= nil, "Option %q is unknown.", 2, option);
	assert(value == nil or type(defaultConfig[option]) == type(value), "Type mismatch: %q expected to be a %s.", 2, option, type(defaultConfig[option]));
	local rkey = assert(id == nil or OR_Rings[id], "Ring index out of bounds.", 2);
	if id == nil then
		configInstance[option] = value;
		if invalidatingOptions[option] then
			for k,v in ipairs(OR_Rings) do
				OR_PendingUpdates[v] = OR_PendingUpdates[v] or OR_Rings[v];
			end
			OR_SyncProps();
		end
	else
		configInstance.RingOptions[rkey .. "#" .. option] = value;
		if invalidatingOptions[option] then
			OR_PendingUpdates[OR_Rings[id]] = OR_PendingUpdates[OR_Rings[id]] or OR_Rings[OR_Rings[id]];
			OR_SyncProps();
		end
	end
end
function OneRing:ClearRingOptions(id)
	assert(type(id) == "number", "Syntax: OneRing:ClearRingOptions(id);", 2);
	local key = assert(OR_Rings[id], "Ring index out of bounds.", 2);
	for k, v in pairs(defaultConfig) do
		configInstance.RingOptions[key .. "#" .. k] = nil;
	end
end
function OneRing:ResetOptions(includePerRing)
	assert(type(includePerRing) == "boolean" or includePerRing == nil, "Syntax: OneRing:ResetOptions([includePerRing])", 2);
	for k in pairs(defaultConfig) do
		configInstance[k] = nil;
	end
	if includePerRing then
		for k in pairs(configInstance.RingOptions) do
			configInstance.RingOptions[k] = nil;
		end
	end
	for k,v in ipairs(OR_Rings) do
		OR_PendingUpdates[v] = OR_PendingUpdates[v] or OR_Rings[v];
	end
	OR_SyncProps();
end
function OneRing:SetRingBinding(id, bind)
	assert(type(id) == "number" and (type(bind) == "string" or bind == false or bind == nil), "Syntax: OneRing:SetRingBinding(id, \"binding\" or false or nil);", 2);
	local key = assert(OR_Rings[id], "Ring index out of range", 2);
	if bind == configInstance.Bindings[key] then return; end
	local obind = OneRing:GetRingBinding(id);
	if obind then OR_KeyBindings[obind] = nil; end
	for i=1,#OR_Rings do
		local ikey, cbind, over = OR_Rings[i], OneRing:GetRingBinding(i);
		if cbind == bind or cbind == obind then
			OR_PendingUpdates[ikey] = OR_Rings[ikey];
			if over and cbind == bind and cbind then
				-- A ring already has an override binding for this key. Get rid of it.
				configInstance.Bindings[ikey] = nil;
			end
		end
	end
	configInstance.Bindings[key], OR_PendingUpdates[key] = bind, OR_Rings[key];
	if not InCombatLockdown() then OR_SyncProps(); end
end
function OneRing:GetRingBinding(id)
	assert(type(id) == "number", "Syntax: binding, override = OneRing:GetRingBinding(id);", 2);
	local rkey = assert(OR_Rings[id], "Ring index out of range", 2);
	local key, over, active = configInstance.Bindings[rkey], true, false;
	if key == nil then key, over = OR_Rings[rkey].hotkey, false; end
	if key then active = OR_KeyBindings[key][1] == rkey or OR_KeyBindings[key][2] == rkey; end
	return key, over, active;
end
function OneRing:GetVersion()
	return GetAddOnMetadata(OR_AddonName, "X-Version-Name-Wrath"), versionMajor, versionRev;
end
function OneRing:RegisterPVar(name, into, notifier)
	assert(type(name) == "string" and (into == nil or type(into) == "table") and (notifier == nil or type(notifier) == "function"), "Syntax: OneRing:RegisterPVar(\"name\"[, storageTable[, notifierFunc]]", 2);
	assert(PersistentStorageInfo[name] == nil, "PVar %q already declared.", 2, name);
	into = into or {};
	PersistentStorageInfo[name], PersistentStorageInfo[into] = into, notifier;
	if PersistentStorageInfo[0] and configInstance.PersistentStorage[name] then
		copy(configInstance.PersistentStorage[name], nil, into);
	end
	if unlocked then
		OR_NotifyPVars("LOADED", into);
	end
	return into;
end

EC_Register("ADDON_LOADED", "ORL.State", OR_LibState);
EC_Register("PLAYER_LOGIN", "ORL.State", OR_LibState);
EC_Register("PLAYER_LOGOUT", "ORL.State", OR_LibState);
EC_Register("PLAYER_REGEN_ENABLED", "ORL.OutOfCombat", OR_SwitchCombatState);
EC_Register("PLAYER_REGEN_DISABLED", "ORL.IntoCombat", OR_SwitchCombatState);
EC_Register("UPDATE_BINDINGS", "ORL.Bindings", OR_CheckBindings);
EC_Register("COMPANION_LEARNED", "ORL.Companions", updateSpellCache);

_G.OneRingLib = OneRing;